플러그인을 사용한 포스트 프로세스 효과
참고
플러그인의 이름을 'LearnShader'라 가정하고 진행한다.
플러그인 설정
LearnShader.uplugin 파일에서 LoadingPhase 설정을 "PostConfigInit"으로 해준다.
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "LearnShader",
"Description": "",
"Category": "Other",
"CreatedBy": "",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "LearnShader",
"Type": "Runtime",
"LoadingPhase": "PostConfigInit"
}
]
}
LearnShader.Build.cs 파일에서 모듈과 IncludePath를 추가해 준다.
...
PrivateIncludePaths.AddRange(
new string[] {
Path.Combine(GetModuleDirectory("Renderer"), "Private")
}
);
...
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"Projects",
"RHI",
"Renderer",
"RenderCore"
}
);
구현
- LearnShader.cpp
void FLearnShaderModule::StartupModule()
{
// 셰이더 소스 폴더 매핑
FString BaseDir = IPluginManager::Get().FindPlugin(TEXT("LearnShader"))->GetBaseDir();
FString PluginShaderDir = FPaths::Combine(BaseDir, TEXT("Shaders"));
if (!AllShaderSourceDirectoryMappings().Contains(PluginShaderDir))
{
AddShaderSourceDirectoryMapping("/LearnShader", PluginShaderDir);
}
}
셰이더 소스가 들어있는 폴더를 "/LearnShader"라는 가상의 경로로 매핑하는 과정이다. 즉
"엔진 경로/플러그인 경로/Shaders" 라는 폴더를 "/LearnShader"라는 경로로 매핑 하는 것이다.
SceneViewExtensionBase
FSceneViewExtensionBase를 상속한 FMySceneViewExtension 클래스를 만들어 준다. 실질적으로 렌더링 코드가 작성될 클래스다. 오버라이드된 함수의 이름마다 호출되는 타이밍이 다르다는 것을 알 수 있다.
- MySceneViewExtension.h
#pragma once
#include "SceneViewExtension.h"
class LEARNSHADER_API FMySceneViewExtension : public FSceneViewExtensionBase
{
public:
FMySceneViewExtension(const FAutoRegister& AutoRegister);
~FMySceneViewExtension();
virtual void SetupViewFamily(FSceneViewFamily& InViewFamily) override;
virtual void SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView) override;
virtual void BeginRenderViewFamily(FSceneViewFamily& InViewFamily) override;
virtual void PreRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView) override;
virtual void PreRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily) override;
virtual void PostRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily) override;
virtual void PostRenderBasePassDeferred_RenderThread(
FRDGBuilder& GraphBuilder,
FSceneView& InView,
const FRenderTargetBindingSlots& RenderTargets,
TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures
) override;
virtual void PrePostProcessPass_RenderThread(
FRDGBuilder& GraphBuilder,
const FSceneView& InView,
const FPostProcessingInputs& Inputs
) override;
};
소스코드 부분은 일단 전체를 확인하고, 부분부분 내용을 확인해 보자.
- MySceneViewExtension.cpp
#include "Render/MySceneViewExtension.h"
#include "Runtime/Renderer/Private/SceneRendering.h"
#include "PostProcess/PostProcessInputs.h"
#include "PixelShaderUtils.h"
BEGIN_SHADER_PARAMETER_STRUCT(FColorExtractParams, )
SHADER_PARAMETER(FVector3f, TargetColor)
SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneColorTexture)
SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
class FMyTestShaderPS : public FGlobalShader
{
public:
DECLARE_EXPORTED_SHADER_TYPE(FMyTestShaderPS, Global, )
using FParameters = FColorExtractParams;
SHADER_USE_PARAMETER_STRUCT(FMyTestShaderPS, FGlobalShader);
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
// SET_SHADER_DEFINE(OutEnvironment, YOUR_SHADER_MACRO, 0);
}
};
IMPLEMENT_SHADER_TYPE(, FMyTestShaderPS, TEXT("/LearnShader/MyTestShader.usf"), TEXT("MainPS"), SF_Pixel);
DECLARE_GPU_DRAWCALL_STAT(ColorMix);
FMySceneViewExtension::FMySceneViewExtension(const FAutoRegister& AutoRegister):
FSceneViewExtensionBase(AutoRegister)
{
}
FMySceneViewExtension::~FMySceneViewExtension()
{
}
void FMySceneViewExtension::SetupViewFamily(FSceneViewFamily& InViewFamily)
{
}
void FMySceneViewExtension::SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView)
{
}
void FMySceneViewExtension::BeginRenderViewFamily(FSceneViewFamily& InViewFamily)
{
}
void FMySceneViewExtension::PreRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView)
{
}
void FMySceneViewExtension::PreRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily)
{
}
void FMySceneViewExtension::PostRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily)
{
}
void FMySceneViewExtension::PostRenderBasePassDeferred_RenderThread(
FRDGBuilder& GraphBuilder,
FSceneView& InView,
const FRenderTargetBindingSlots& RenderTargets,
TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures
)
{
}
void FMySceneViewExtension::PrePostProcessPass_RenderThread(
FRDGBuilder& GraphBuilder,
const FSceneView& InView,
const FPostProcessingInputs& Inputs
)
{
FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, InView, Inputs);
// Unreal Insights
RDG_GPU_STAT_SCOPE(GraphBuilder, ColorMix);
// Render Doc
RDG_EVENT_SCOPE(GraphBuilder, "ColorMix");
// Grab the Scene Texture
const FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(
GraphBuilder,
InView,
ESceneTextureSetupMode::SceneColor | ESceneTextureSetupMode::GBuffers
);
// This is the color that actually has the shadow and the shade
check(InView.bIsViewInfo);
const FIntRect Viewport = static_cast<const FViewInfo&>(InView).ViewRect;
const FScreenPassTexture SceneColorTexture((*Inputs.SceneTextures)->SceneColorTexture, Viewport);
// Set global shader data, allocate memory
FMyTestShaderPS::FParameters* Parameters = GraphBuilder.AllocParameters<FMyTestShaderPS::FParameters>();
Parameters->SceneColorTexture = SceneColorTexture.Texture;
Parameters->SceneTextures = SceneTextures;
Parameters->TargetColor = FVector3f(1.0f, 0.0f, 1.0f);
// Set RenderTarget and Return Texture
Parameters->RenderTargets[0] = FRenderTargetBinding(
(*Inputs.SceneTextures)->SceneColorTexture,
ERenderTargetLoadAction::ELoad
);
const FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
TShaderMapRef<FMyTestShaderPS> PixelShader(GlobalShaderMap);
FPixelShaderUtils::AddFullscreenPass(
GraphBuilder,
GlobalShaderMap,
FRDGEventName(TEXT("Color Mix Pass")),
PixelShader,
Parameters,
Viewport
);
}
// https://illu.tistory.com/1500
먼저 셰이더 파라미터를 선언하는 부분이다. 상수 버퍼를 포함한 셰이더에 필요한 파라미터를 설정하는 부분이다.
BEGIN_SHADER_PARAMETER_STRUCT(FColorExtractParams, )
SHADER_PARAMETER(FVector3f, TargetColor)
SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneColorTexture)
SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
BEGIN_SHADER_PARAMETER_STRUCT매크로로 구조체를 시작하고,END_SHADER_PARAMETER_STRUCT()로 구조체를 끝낸다.SHADER_PARAMETER계열의 매크로를 통해 멤버가 선언이 가능하다.SHADER_PARAMETER_STRUCT_INCLUDE는 기존의 셰이더 파라미터 구조체를 이 셰이더 파라미터 구조체에서 사용하게 할 수 있다. 여기선 사용하지 않고, 셰이더 소스에서도 선언되지 않을 예정이므로 주석처리해도 무방하다.RENDER_TARGET_BINDING_SLOTS를 통해 렌더 타켓을 바인딩한다.
FMyTestShaderPS 클래스를 통해 셰이더 내용을 작성한다.
class FMyTestShaderPS : public FGlobalShader
{
public:
DECLARE_EXPORTED_SHADER_TYPE(FMyTestShaderPS, Global, )
using FParameters = FColorExtractParams;
SHADER_USE_PARAMETER_STRUCT(FMyTestShaderPS, FGlobalShader);
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
// SET_SHADER_DEFINE(OutEnvironment, YOUR_SHADER_MACRO, 0);
}
};
IMPLEMENT_SHADER_TYPE(, FMyTestShaderPS, TEXT("/LearnShader/MyTestShader.usf"), TEXT("MainPS"), SF_Pixel);
DECLARE_EXPORTED_SHADER_TYPE는 셰이더 유형을 선언하는 데 사용하며, 여기서는 GlobalShader로 선언된다. 동시에, 일부 필수 코드 또한 생성한다.using FParameters = FColorExtractParams;를 통해 셰이더 파라미터 별칭을FParameters로 설정하며,SHADER_USE_PARAMETER_STRUCT(FMyTestShaderPS, FGlobalShader);매크로가 FParameters를 통해 셰이더 파라미터를 정의한다. 여기서, 이 두 조합을 사용 하지 않고FColorExtractParams구조체 + 추가적인 파라미터로 셰이더 파라미터를 구성할 수 있으니 이글의class FLensFlareRescalePS클래스를 참고.ShouldCompilePermutation함수는 쉽게 생각하면 셰이더를 컴파일 할 지를 결정 하는 것이다. 현재는 SM5가 지원될 때만 컴파일이 되게 하고 있으며 오버라이딩 하지 않을 시 FShader의 해당 함수는 무조건 true를 반환하도록 되어 있다.ModifyCompilationEnvironment함수에서는 셰이더에 define을 할 수 있다. 즉 이 부분을 통해 Permutation을 구현 할 수 있다.IMPLEMENT_SHADER_TYPE함수는FMyTestShaderPS클래스의 함수를 구현하는 매크로다. 첫번째 인자는 함수의 prefix(static, inline 같은)를 설정하는 것이고 두번째 인자는 셰이더 클래스를 이다. 세번째 인자는 셰이더 파일의 경로를 설정하는 것이고, 네번째 함수는 메인 함수의 이름을, 마지막은 셰이더 유형을 설정한다.
성능 추적을 위한 함수들을 정의하는 매크로다.
DECLARE_GPU_DRAWCALL_STAT(ColorMix);
아래에서 사용될
RDG_GPU_STAT_SCOPE(GraphBuilder, ColorMix);
매크로 사용을 위해 필요하다.
FMySceneViewExtension::PrePostProcessPass_RenderThread 함수는 차례대로 살펴본다.
FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, InView, Inputs);
부모의 함수를 호출한다. 실질적으로 실행되는 것은 아무것도 없는 함수다. 생략해도 무관하지만, 이후 언리얼 엔진의 버전이 달라짐에 따라 어떻게 될지 모른다.
성능 체크를 위한 매크로를 사용한다.
// Unreal Insights
RDG_GPU_STAT_SCOPE(GraphBuilder, ColorMix);
// Render Doc
RDG_EVENT_SCOPE(GraphBuilder, "ColorMix");
주석에 나온 것 처럼, RDG_GPU_STAT_SCOPE 는 Unreal Insights를 위한 매크로 이고 RDG_EVENT_SCOPE 는 Render Doc을 위한 매크로다. RDG_GPU_STAT_SCOPE 매크로는 위에 선언한 DECLARE_GPU_DRAWCALL_STAT가 없을 경우 컴파일이 안되므로 주의
셰이더 파라미터를 설정하는 부분이다.
// Grab the Scene Texture
const FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(
GraphBuilder,
InView,
ESceneTextureSetupMode::SceneColor | ESceneTextureSetupMode::GBuffers
);
// This is the color that actually has the shadow and the shade
check(InView.bIsViewInfo);
const FIntRect Viewport = static_cast<const FViewInfo&>(InView).ViewRect;
const FScreenPassTexture SceneColorTexture((*Inputs.SceneTextures)->SceneColorTexture, Viewport);
// Set global shader data, allocate memory
FMyTestShaderPS::FParameters* Parameters = GraphBuilder.AllocParameters<FMyTestShaderPS::FParameters>();
Parameters->SceneColorTexture = SceneColorTexture.Texture;
Parameters->SceneTextures = SceneTextures;
Parameters->TargetColor = FVector3f(1.0f, 0.0f, 1.0f);
// Set RenderTarget and Return Texture
Parameters->RenderTargets[0] = FRenderTargetBinding(
(*Inputs.SceneTextures)->SceneColorTexture,
ERenderTargetLoadAction::ELoad
);
다른 부분은 읽으면 알 수 있는 부분이라 생각하고,
check(InView.bIsViewInfo);
const FIntRect Viewport = static_cast<const FViewInfo&>(InView).ViewRect;
이부분의 경우 InView의 타입인 FSceneView가 FViewInfo의 부모 클래스 이기 때문에 위험한 변환이지만,
check(InView.bIsViewInfo); 를 통해 타입을 체크 후 하는 변환이므로 사실상 안전하게 변환한다고 볼 수 있다.
마지막으로 셰이더를 호출하고, 드로우를 하는 부분이다.
const FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
TShaderMapRef<FMyTestShaderPS> PixelShader(GlobalShaderMap);
FPixelShaderUtils::AddFullscreenPass(
GraphBuilder,
GlobalShaderMap,
FRDGEventName(TEXT("Color Mix Pass")),
PixelShader,
Parameters,
Viewport
);
이전에 설정한 값들을 그대로 보내주는데, FRDGEventName(TEXT("Color Mix Pass"))의 경우는 RenderDoc에서 해당 드로우 이벤트를 "Color Mix Pass"로 표시하게 될 것 이다.
Shader Source
간단한 셰이더 소스를 만든다.
#include "/Engine/Public/Platform.ush"
float3 TargetColor;
Texture2D<float4> SceneColorTexture;
float4 MainPS(float4 SvPosition : SV_Position) : SV_Target
{
const float4 SceneColor = SceneColorTexture.Load(int3(SvPosition.xy, 0.0f));
const float3 MainColor = SceneColor.rgb * TargetColor;
return float4(MainColor, 1.0f);
}
#include "/Engine/Public/Platform.ush"
위 헤더파일을 포함해야 컴파일이 가능하다. Common.ush에 해당 헤더가 포함되어 있으니 이것을 포함시키는 것도 방법.
float3 TargetColor;
Texture2D<float4> SceneColorTexture;
FColorExtractParams클래스에서 선언한 파라미터에 대응되는 값이다.
float4 MainPS(float4 SvPosition : SV_Position) : SV_Target
{
const float4 SceneColor = SceneColorTexture.Load(int3(SvPosition.xy, 0.0f));
const float3 MainColor = SceneColor.rgb * TargetColor;
return float4(MainColor, 1.0f);
}
메인 함수다. 별 내용 없이 단순한 함수다.
EngineSubSystem
UEngineSubsystem을 상속하는 UMyEngineSubsystem라는 클래스를 만들어 준다. 서브시스템은 언리얼 엔진의 싱글톤 클래스다. 이 클래스에서 FMySceneViewExtension의 인스턴스를 만들어 줄 것 이다.
- MyEnginSubsystem.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/EngineSubsystem.h"
#include "MyEngineSubsystem.generated.h"
class FSceneViewExtensionBase;
UCLASS()
class LEARNSHADER_API UMyEngineSubsystem : public UEngineSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
private:
TSharedPtr<FSceneViewExtensionBase, ESPMode::ThreadSafe> CustomSceneViewExtension;
};
- MyEnginSubsystem.cpp
#include "SubSystems/MyEngineSubsystem.h"
#include "Render/MySceneViewExtension.h"
void UMyEngineSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
CustomSceneViewExtension = FSceneViewExtensions::NewExtension<FMySceneViewExtension>();
}
void UMyEngineSubsystem::Deinitialize()
{
CustomSceneViewExtension.Reset();
CustomSceneViewExtension = nullptr;
Super::Deinitialize();
}
특별한 내용 없고 FMySceneViewExtension를 만들고, 소멸시키는 내용이다.
프로젝트를 실행해서 확인하면 다음과 같은 화면을 볼 수 있다.

Unreal Insights에서도 Color Mix 항목을 찾을 수 있다.
